﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Text;
using System.Windows.Forms;
using POS.USBScanner.Configuration;

namespace POS.USBScanner
{
    public interface IBarcodeScannerListener
    {
        event EventHandler BarcodeScanned;
    }

    public class BarcodeScannerListener : NativeWindow
    {
        private readonly Dictionary<IntPtr, BarcodeScannerDeviceInfo> _devices;

        private StringBuilder _keystrokeBuffer;

        public BarcodeScannerListener()
        {
            IntPtr hwnd;
            
            hwnd = NativeMethods.GetActiveWindow();

            if(hwnd == IntPtr.Zero)
            {
                throw new InvalidOperationException("No active window");
            }

            this._devices = new Dictionary<IntPtr, BarcodeScannerDeviceInfo>();
            this._keystrokeBuffer = new StringBuilder();

            this.InitializeBarcodeScannerDeviceHandles();
            HookRawInput(hwnd);
            //this.HookHandleEvents(form);

            this.AssignHandle(hwnd);
        }

        public event EventHandler BarcodeScanned;

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        protected override void WndProc(ref Message m)
        {

            switch ((uint)m.Msg)
            {
                case (uint)WindowsMessages.WM_INPUT:
                    if (this.ProcessRawInputMessage(m.LParam))
                    {
                        NativeMethods.MSG message;
                        NativeMethods.PeekMessage(
                            out message,
                            IntPtr.Zero,
                            (uint)WindowsMessages.WM_KEYUP,
                            (uint)WindowsMessages.WM_KEYUP,
                            NativeMethods.PeekMessageRemoveFlag.PM_REMOVE);
                    }
                    else
                        base.WndProc(ref m);


                    break;
                default:
                    {
                        base.WndProc(ref m);
                        break;
                    }
            }

            
        }

        private static BarcodeScannerDeviceType GetBarcodeScannerDeviceType(NativeMethods.RawInputType rawInputType)
        {
            BarcodeScannerDeviceType type;

            switch (rawInputType)
            {
                case NativeMethods.RawInputType.RIM_TYPEHID:
                    type = BarcodeScannerDeviceType.HumanInterfaceDevice;
                    break;
                case NativeMethods.RawInputType.RIM_TYPEKEYBOARD:
                    type = BarcodeScannerDeviceType.Keyboard;
                    break;
                default:
                    type = BarcodeScannerDeviceType.Unknown;
                    break;
            }

            return type;
        }

        private static string GetDeviceFriendlyName(string dbccName)
        {
            string[] dbccParts;
            string deviceInstanceId;
            string friendlyName;
            NativeMethods.SetupDiGetClassDevsFlags getClassDevsFlags;
            IntPtr devInfo;
            Guid classGuid;

            classGuid = new Guid(NativeMethods.GUID_DEVINTERFACE_HID.ToByteArray());
            friendlyName = string.Empty;

            dbccParts = dbccName.Split('#');
            deviceInstanceId = (dbccParts[0].Substring(dbccParts[0].LastIndexOf(@"\", StringComparison.Ordinal) + 1) +
                @"\" +
                dbccParts[1] +
                @"\" +
                dbccParts[2]).ToUpperInvariant();
            getClassDevsFlags = 
                NativeMethods.SetupDiGetClassDevsFlags.DIGCF_PRESENT | 
                NativeMethods.SetupDiGetClassDevsFlags.DIGCF_DEVICEINTERFACE;

           devInfo = NativeMethods.SetupDiGetClassDevs(
                ref classGuid,
                null,
                IntPtr.Zero,
                getClassDevsFlags);
            if (!devInfo.Equals(NativeMethods.INVALID_HANDLE_VALUE))
            {
                NativeMethods.SP_DEVINFO_DATA devInfoData;

                devInfoData = new NativeMethods.SP_DEVINFO_DATA();
                devInfoData.cbSize = (uint)Marshal.SizeOf(devInfoData);

                
                for (uint i = 0; NativeMethods.SetupDiEnumDeviceInfo(devInfo, i, ref devInfoData); ++i)
                {
                    IntPtr buffer;
                    int bufferSize;
                    string currentDeviceInstanceId;
                    int requiredSize;

                    bufferSize = 1024;
                    buffer = Marshal.AllocHGlobal(bufferSize);
                    requiredSize = 0;

                    // TODO: This returns ERROR_INVALID_PARAMETER
                    if (NativeMethods.SetupDiGetDeviceInstanceId(
                        devInfo,
                        ref devInfoData,
                        buffer,
                        bufferSize,
                        out requiredSize))
                    {
                        currentDeviceInstanceId = Marshal.PtrToStringAnsi(buffer);

                        if (currentDeviceInstanceId == deviceInstanceId)
                        {
                            IntPtr propertyBuffer;
                            uint regType;
                            int propertyBufferSize;
                            uint propertyBufferSizeRequiredSize;

                            propertyBufferSize = 1024;
                            propertyBuffer = Marshal.AllocHGlobal(propertyBufferSize);

                            // We found our device!
                            if (NativeMethods.SetupDiGetDeviceRegistryProperty(
                                devInfo,
                                ref devInfoData,
                                NativeMethods.SetupDiGetDeviceRegistryPropertyType.SPDRP_FRIENDLYNAME,
                                out regType,
                                propertyBuffer,
                                (uint)propertyBufferSize,
                                out propertyBufferSizeRequiredSize) ||
                                NativeMethods.SetupDiGetDeviceRegistryProperty(
                                devInfo,
                                ref devInfoData,
                                NativeMethods.SetupDiGetDeviceRegistryPropertyType.SPDRP_DEVICEDESC,
                                out regType,
                                propertyBuffer,
                                (uint)propertyBufferSize,
                                out propertyBufferSizeRequiredSize))
                            {
                                friendlyName = Marshal.PtrToStringAnsi(propertyBuffer);
                            }

                            Marshal.FreeHGlobal(propertyBuffer);
                        }
                    }

                    Marshal.FreeHGlobal(buffer);
                    
                }

                NativeMethods.SetupDiDestroyDeviceInfoList(devInfo);
            }

            return friendlyName;
        }

        private static void HookRawInput(IntPtr hwnd)
        {
            NativeMethods.RAWINPUTDEVICE[] rid;

            rid = new NativeMethods.RAWINPUTDEVICE[1];

            rid[0].usUsagePage = 0x01;      // USB HID Generid Desktop Page
            rid[0].usUsage = 0x06;          // Keyboard Usage ID
            rid[0].dwFlags = NativeMethods.RawInputDeviceFlags.RIDEV_INPUTSINK;
            rid[0].hwndTarget = hwnd;

            if (!NativeMethods.RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(rid[0])))
            {
                InvalidOperationException e;

                e = new InvalidOperationException(
                    "The barcode scanner listener could not register for raw input devices.",
                    new Win32Exception());
                throw e;
            }
        }

        private void FireBarcodeScanned(BarcodeScannerDeviceInfo deviceInfo)
        {
            string barcode;
            EventHandler handler;

            barcode = this._keystrokeBuffer.ToString();
            handler = this.BarcodeScanned;

            this._keystrokeBuffer = new StringBuilder();

            if (handler != null)
            {
                handler(this, new BarcodeScannedEventArgs(barcode, deviceInfo));
                handler -= this.BarcodeScanned;
            }
            
        }

        private void InitializeBarcodeScannerDeviceHandles()
        {
            BarcodeScannerListenerConfigurationSection config;
            BarcodeScannerListenerConfigurationElementCollection hardwareIdsConfig;
            List<string> hardwareIds;
            uint numDevices;
            uint size;

            config = BarcodeScannerListenerConfigurationSection.GetConfiguration();
            hardwareIdsConfig = config.HardwareIds;
            hardwareIds = new List<string>();
            numDevices = 0;
            size = (uint)Marshal.SizeOf(typeof(NativeMethods.RAWINPUTDEVICELIST));

            foreach (BarcodeScannerListenerConfigurationElement hardwareId in hardwareIdsConfig)
            {
                hardwareIds.Add(hardwareId.Id);
            }

            // First, we get the number of raw input devices in the list by passing
            // in IntPtr.Zero. Then we allocate sufficient memory and retrieve the
            // entire list.
            if (NativeMethods.GetRawInputDeviceList(IntPtr.Zero, ref numDevices, size) == 0)
            {
                IntPtr rawInputDeviceList;

                rawInputDeviceList = Marshal.AllocHGlobal((int)(size * numDevices));
                if (NativeMethods.GetRawInputDeviceList(
                    rawInputDeviceList,
                    ref numDevices,
                    size) != uint.MaxValue)
                {
                    // Next, we iterate through the list, discarding undesired items
                    // and retrieving further information on the barcode scanner devices
                    for (int i = 0; i < numDevices; ++i)
                    {
                        uint pcbSize;
                        NativeMethods.RAWINPUTDEVICELIST rid;

                        pcbSize = 0;
                        rid = (NativeMethods.RAWINPUTDEVICELIST)Marshal.PtrToStructure(
                            new IntPtr((rawInputDeviceList.ToInt32() + (size * i))),
                            typeof(NativeMethods.RAWINPUTDEVICELIST));

                        if (NativeMethods.GetRawInputDeviceInfo(
                            rid.hDevice,
                            NativeMethods.RawInputDeviceInfoCommand.RIDI_DEVICENAME,
                            IntPtr.Zero,
                            ref pcbSize) >= 0)
                        {
                            if (pcbSize > 0)
                            {
                                string deviceName;
                                string friendlyName;
                                BarcodeScannerDeviceInfo info;
                                IntPtr data;

                                data = Marshal.AllocHGlobal((int)pcbSize);
                                if (NativeMethods.GetRawInputDeviceInfo(
                                    rid.hDevice,
                                    NativeMethods.RawInputDeviceInfoCommand.RIDI_DEVICENAME,
                                    data,
                                    ref pcbSize) >= 0)
                                {
                                    deviceName = Marshal.PtrToStringAnsi(data);
                                    Marshal.FreeHGlobal(data);      // Don't want to keep this for too long

                                    if ((from hardwareId in hardwareIds
                                         where deviceName.Contains(hardwareId)
                                         select hardwareId).Any())
                                    {
                                        friendlyName = GetDeviceFriendlyName(deviceName);

                                        info = new BarcodeScannerDeviceInfo(
                                            deviceName,
                                            GetBarcodeScannerDeviceType(rid.dwType),
                                            rid.hDevice,
                                            friendlyName);

                                        this._devices.Add(rid.hDevice, info);
                                    }
                                }
                            }
                        }
                    }
                }

                Marshal.FreeHGlobal(rawInputDeviceList);
            }
        }

        private bool ProcessRawInputMessage(IntPtr rawInputHeader)
        {

            bool handled = false;
            uint size = 0;
            // First we call GetRawInputData() to set the value of size, which
            // we will the nuse to allocate the appropriate amount of memory in
            // the buffer.
            if (NativeMethods.GetRawInputData(
                    rawInputHeader,
                    NativeMethods.RawInputCommandFlag.RID_INPUT,
                    IntPtr.Zero,
                    ref size,
                    (uint)Marshal.SizeOf(typeof(NativeMethods.RAWINPUTHEADER))) == 0)
            {
                IntPtr buffer = IntPtr.Zero;
                BarcodeScannerDeviceInfo deviceInfo;

                if (size != 0)
                {
                    buffer = Marshal.AllocHGlobal((int)size);
                   try
                   {
                        if (NativeMethods.GetRawInputData(
                                rawInputHeader,
                                NativeMethods.RawInputCommandFlag.RID_INPUT,
                                buffer,
                                ref size,
                                (uint)Marshal.SizeOf(typeof(NativeMethods.RAWINPUTHEADER))) == size)
                        {
                            if (buffer != IntPtr.Zero)
                            {
                                NativeMethods.RAWINPUT raw = (NativeMethods.RAWINPUT)Marshal.PtrToStructure(buffer, typeof(NativeMethods.RAWINPUT));
                                
                                if (this._devices.TryGetValue(raw.header.hDevice, out deviceInfo))
                                {
                                    handled = true;

                                    if (raw.header.dwType == NativeMethods.RawInputType.RIM_TYPEKEYBOARD)
                                    {

                                        if (raw.Data.keyboard.Message == (uint)WindowsMessages.WM_KEYUP)
                                        {
                                            byte[] state = new byte[256];
                                            char[] localBuffer = new char[1];
                                            if (NativeMethods.GetKeyboardState(state))
                                            {
                                                if (NativeMethods.ToUnicode(
                                                        raw.Data.keyboard.VKey,
                                                        raw.Data.keyboard.MakeCode,
                                                        state,
                                                        localBuffer,
                                                        localBuffer.Length,
                                                        0) == 1)
                                                {
                                                    if (localBuffer.Length == 1 && (localBuffer[0] == 13 || localBuffer[0] == 10))    // Look for an LF ('\r') or a CR ('\n') depending on what the scanner gives us
                                                    {
  //                                                      this.keystrokeBuffer.Append(localBuffer[0]);
                                                        System.Diagnostics.Debug.WriteLine("Barcode Scanned: " + this._keystrokeBuffer.ToString() + " Firing event...");
                                                        this.FireBarcodeScanned(deviceInfo);
                                                        

                                                    }
                                                    else
                                                    {
                                                        this._keystrokeBuffer.Append(localBuffer[0]);

                                                    }
                                                }
                                            }
                                        }
                                    }
                                    else
                                    {
                                        Marshal.FreeHGlobal(buffer);
                                        buffer = IntPtr.Zero;
                                    }
                                }
                            }
                        }
                    }

                    finally
                    {

                        if (buffer != IntPtr.Zero)
                        {
//                            System.Diagnostics.Debug.WriteLine(@"Freeing buffer");
                            Marshal.FreeHGlobal(buffer);
                        }
                    }
                }
            }

            return handled;
        }
    }
}
